package com.katesoft.scale4j.agent;

import static com.katesoft.scale4j.common.io.FileUtility.PATH_SEPARATOR;
import static com.katesoft.scale4j.common.io.FileUtility.SEPARATOR;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.SystemUtils;

import com.katesoft.scale4j.common.lang.OS;
import com.katesoft.scale4j.common.lang.RuntimeUtility;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.xml.jvmcluster.ApplicationArgType;
import com.katesoft.xml.jvmcluster.ApplicationConfigurationDocument.ApplicationConfiguration;
import com.katesoft.xml.jvmcluster.JvmArgType;
import com.katesoft.xml.jvmcluster.JvmConfigurationDocument.JvmConfiguration;
import com.katesoft.xml.jvmcluster.JvmConfigurationDocument.JvmConfiguration.VmType;
import com.katesoft.xml.jvmcluster.ServiceDocument.Service;
import com.katesoft.xml.jvmcluster.SystemPropertiesType;

/**
 * Similar to {@link ProcessBuilder}, this class makes the java process creation easier.
 * 
 * @author kate2007
 */
public class JavaProcessBuilder {
   private final Logger logger = LogFactory.getLogger(getClass());
   private final File javaHome = SystemUtils.getJavaHome();
   private File workingDir = SystemUtils.getUserDir();
   private final List<String> classpath = new LinkedList<String>();
   private final List<String> endorsedDirs = new LinkedList<String>();
   private final List<String> extDirs = new LinkedList<String>();
   private final List<String> libraryPath = new LinkedList<String>();
   private final List<String> bootClasspath = new LinkedList<String>();
   private final List<String> appendBootClasspath = new LinkedList<String>();
   private final List<String> prependBootClasspath = new LinkedList<String>();
   private final List<String> jvmArgs = new LinkedList<String>();
   private final List<String> args = new LinkedList<String>();
   private final Map<String, String> systemProperties = new HashMap<String, String>();
   private String initialHeap;
   private String maxHeap;
   private String vmType;
   private String mainClass;
   private boolean debugSuspend;
   private int debugPort = -1;

   public JavaProcessBuilder() {
      super();
   }

   public JavaProcessBuilder(ApplicationConfiguration cfg, Service service) {
      mainClass(service.getMainClass());
      applyJvmConfiguration(service.getJvmConfiguration());
      if (!ArrayUtils.isEmpty(service.getApplicationArgArray())) {
         for (ApplicationArgType arg : service.getApplicationArgArray()) {
            arg(arg.getValue());
         }
      }
      if (cfg.getGlobalConfiguration() != null) {
         addJvmArgs(cfg.getGlobalConfiguration().getJvmArgArray());
         addSystemProperties(cfg.getGlobalConfiguration().getSystemPropertyArray());
      }
      systemProperty("com.sun.management.jmxremote.port",
               String.valueOf(service.getJmxPort().intValue()));
      systemProperty("com.sun.management.jmxremote", null);
   }

   public void applyJvmConfiguration(JvmConfiguration jvmConfiguration) {
      if (jvmConfiguration != null) {
         if (jvmConfiguration.getMemoryConfiguration() != null) {
            if (jvmConfiguration.getMemoryConfiguration().isSetInitialHeap()) {
               initialHeap(jvmConfiguration.getMemoryConfiguration().getInitialHeap().intValue());
            }
            if (jvmConfiguration.getMemoryConfiguration().isSetMaxHeap()) {
               maxHeap(jvmConfiguration.getMemoryConfiguration().getMaxHeap().intValue());
            }
         }
         if (jvmConfiguration.getBootClasspathConfiguration() != null) {
            if (jvmConfiguration.getBootClasspathConfiguration().isSetPrependBootClasspath()) {
               prependBootClasspath(jvmConfiguration.getBootClasspathConfiguration()
                        .getPrependBootClasspath());
            }
            if (jvmConfiguration.getBootClasspathConfiguration().isSetBootClasspath()) {
               bootClasspath(jvmConfiguration.getBootClasspathConfiguration().getBootClasspath());
            }
            if (jvmConfiguration.getBootClasspathConfiguration().isSetAppendBootClasspath()) {
               appendBootClasspath(jvmConfiguration.getBootClasspathConfiguration()
                        .getAppendBootClasspath());
            }
         }
         if (jvmConfiguration.getExtPathConfiguration() != null) {
            if (jvmConfiguration.getExtPathConfiguration().isSetEndorsedDirs()) {
               endorsedDir(jvmConfiguration.getExtPathConfiguration().getEndorsedDirs());
            }
            if (jvmConfiguration.getExtPathConfiguration().isSetExtDirs()) {
               extDir(jvmConfiguration.getExtPathConfiguration().getExtDirs());
            }
            if (jvmConfiguration.getExtPathConfiguration().isSetLibraryPath()) {
               libraryPath(jvmConfiguration.getExtPathConfiguration().getLibraryPath());
            }
         }
         if (jvmConfiguration.getDebugConfiguration() != null) {
            debugPort(jvmConfiguration.getDebugConfiguration().getPort());
            debugSuspend(jvmConfiguration.getDebugConfiguration().getDebugSuspend());
         }
         if (jvmConfiguration.getVmType() != null) {
            if (VmType.SERVER == jvmConfiguration.getVmType()) {
               server();
            } else if (VmType.CLIENT == jvmConfiguration.getVmType()) {
               client();
            }
         }
         if (jvmConfiguration.isSetAdditionalClasspath()) {
            classpath(jvmConfiguration.getAdditionalClasspath());
         }
         addJvmArgs(jvmConfiguration.getJvmArgArray());
         addSystemProperties(jvmConfiguration.getSystemPropertyArray());
      }
   }

   private void addJvmArgs(JvmArgType[] jvmArgArray) {
      if (!ArrayUtils.isEmpty(jvmArgArray)) {
         for (JvmArgType arg : jvmArgArray) {
            jvmArg(arg.getValue());
         }
      }
   }

   private void addSystemProperties(SystemPropertiesType[] array) {
      if (!ArrayUtils.isEmpty(array)) {
         for (SystemPropertiesType arg : array) {
            systemProperty(arg.getName(), arg.getValue());
         }
      }
   }

   public File javaHome() {
      return javaHome;
   }

   public JavaProcessBuilder workingDir(String dir) {
      return workingDir(new File(dir));
   }

   public JavaProcessBuilder workingDir(File dir) {
      workingDir = dir;
      return this;
   }

   public JavaProcessBuilder classpath(String resource) {
      classpath.add(resource);
      return this;
   }

   public JavaProcessBuilder endorsedDir(String dir) {
      endorsedDirs.add(dir);
      return this;
   }

   public JavaProcessBuilder extDir(String dir) {
      extDirs.add(dir);
      return this;
   }

   public JavaProcessBuilder libraryPath(String dir) {
      libraryPath.add(dir);
      return this;
   }

   public JavaProcessBuilder bootClasspath(String resource) {
      bootClasspath.add(resource);
      return this;
   }

   public JavaProcessBuilder appendBootClasspath(String resource) {
      appendBootClasspath.add(resource);
      return this;
   }

   public JavaProcessBuilder prependBootClasspath(String resource) {
      prependBootClasspath.add(resource);
      return this;
   }

   public JavaProcessBuilder systemProperty(String name, String value) {
      systemProperties.put(name, value);
      return this;
   }

   public JavaProcessBuilder initialHeap(int mb) {
      return initialHeap(mb + "m");
   }

   public JavaProcessBuilder initialHeap(String size) {
      initialHeap = size;
      return this;
   }

   public JavaProcessBuilder maxHeap(int mb) {
      return maxHeap(mb + "m");
   }

   public JavaProcessBuilder maxHeap(String size) {
      maxHeap = size;
      return this;
   }

   public JavaProcessBuilder client() {
      vmType = "-client";
      return this;
   }

   public JavaProcessBuilder server() {
      vmType = "-server";
      return this;
   }

   public JavaProcessBuilder jvmArg(String arg) {
      jvmArgs.add(arg);
      return this;
   }

   public JavaProcessBuilder mainClass(String mainClass) {
      this.mainClass = mainClass;
      return this;
   }

   public JavaProcessBuilder debugSuspend(boolean suspend) {
      this.debugSuspend = suspend;
      return this;
   }

   public JavaProcessBuilder debugPort(int port) {
      this.debugPort = port;
      return this;
   }

   public JavaProcessBuilder arg(String arg) {
      args.add(arg);
      return this;
   }

   private static String toString(Iterable<String> paths) throws IOException {
      StringBuilder buff = new StringBuilder();
      for (String p : paths) {
         if (buff.length() > 0 && !p.endsWith(PATH_SEPARATOR)) {
            buff.append(PATH_SEPARATOR);
         }
         buff.append(p);
      }
      return buff.toString();
   }

   public String[] command() throws IOException {
      List<String> cmd = new ArrayList<String>();
      String executable = javaHome.getCanonicalPath() + SEPARATOR + "bin" + SEPARATOR + "java";
      if (OS.get().isWindows()) {
         executable += ".exe";
      }
      cmd.add(executable);
      String path = toString(prependBootClasspath);
      if (!path.isEmpty()) {
         cmd.add("-Xbootclasspath/p:" + path);
      }
      path = toString(bootClasspath);
      if (!path.isEmpty()) {
         cmd.add("-Xbootclasspath:" + path);
      }
      path = toString(appendBootClasspath);
      if (!path.isEmpty()) {
         cmd.add("-Xbootclasspath/a:" + path);
      }
      path = toString(classpath);
      if (!path.isEmpty()) {
         cmd.add("-classpath");
         cmd.add(path);
      }
      path = toString(extDirs);
      if (!path.isEmpty()) {
         cmd.add("-Djava.ext.dirs=" + path);
      }
      path = toString(endorsedDirs);
      if (!path.isEmpty()) {
         cmd.add("-Djava.endorsed.dirs=" + path);
      }
      path = toString(libraryPath);
      if (!path.isEmpty()) {
         cmd.add("-Djava.library.path=" + path);
      }
      //
      for (Map.Entry<String, String> prop : systemProperties.entrySet()) {
         if (prop.getValue() == null) {
            cmd.add("-D" + prop.getKey());
         } else {
            cmd.add("-D" + prop.getKey() + "=" + prop.getValue());
         }
      }
      if (initialHeap != null) {
         cmd.add("-Xms" + initialHeap);
      }
      if (maxHeap != null) {
         cmd.add("-Xmx" + maxHeap);
      }
      if (vmType != null) {
         cmd.add(vmType);
      }
      if (debugPort > 0) {
         cmd.add("-Xdebug");
         cmd.add("-Xnoagent");
         cmd.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=" + (debugSuspend ? 'y' : 'n')
                  + ",address=" + debugPort);
      }
      cmd.addAll(jvmArgs);
      if (mainClass != null) {
         cmd.add(mainClass);
         cmd.addAll(args);
      }
      return cmd.toArray(new String[cmd.size()]);
   }

   /**
    * launches jvm with current configuration.
    * <p/>
    * note that, the streams passed are not closed automatically.
    * 
    * @param output
    *           outputstream to which process's input stream to be redirected. if null, it is not
    *           redirected
    * @param error
    *           outputstream to which process's error stream to be redirected. if null, it is not
    *           redirected
    * @return the process created
    * @throws IOException
    *            if an I/O error occurs.
    */
   public Process launch(OutputStream output, OutputStream error) throws IOException {
      String[] command = command();
      logger.info("working_dir = %s, command = %s", workingDir.getCanonicalPath(),
               Arrays.toString(command));
      ProcessBuilder processBuilder = new ProcessBuilder();
      processBuilder.command(command);
      processBuilder.directory(workingDir);
      Process process = processBuilder.start();
      RuntimeUtility.redirectStreams(process, output, error);
      return process;
   }
}
